package me.w70.fps.raycast;

import java.util.ArrayList;
import java.util.List;

import me.w70.fps.actor.Player;
import me.w70.fps.constants.Angles;
import me.w70.fps.map.Cell;
import me.w70.fps.map.Map;
import me.w70.fps.render.GameWindow;

public class Raycaster {
	// Map data
	protected Map mapptr;
	protected final int TILE_SIZE;
	protected Cell[] map;
	protected int mapx;
	protected int mapy;
	// Constant angles used.
	protected Angles angles;
	// Rendering object.
	protected GameWindow renderer;
    // Precalculated trig tables.
    public float[] sintable;
    public float[] inversesintable;
    public float[] costable;
    public float[] inversecostable;
    public float[] tantable;
    public float[] inversetantable;
    public float[] xsteptable;
    public float[] ysteptable;
    private int castangle;
    // Should the renderer use images?
    private boolean useimages;

    // Setup variables, prepare trig tables to be built.
	public Raycaster(Angles anglesobject, Map map, GameWindow renderer, boolean useimages) {
		this.angles = anglesobject;
		this.mapptr = map;
		this.renderer = renderer;
		this.TILE_SIZE = map.TILE_SIZE;
		this.useimages = useimages;
		this.map = map.getMap();
		this.mapx = map.getMapx();
		this.mapy = map.getMapy(); 
		this.sintable = new float[this.angles.ANGLE360 + 1];
		this.inversesintable = new float[this.angles.ANGLE360 + 1];
		this.costable = new float[this.angles.ANGLE360 + 1];
		this.inversecostable = new float[this.angles.ANGLE360 + 1];
		this.tantable = new float[this.angles.ANGLE360 + 1];
		this.inversetantable = new float[this.angles.ANGLE360 + 1];
		this.xsteptable = new float[this.angles.ANGLE360 + 1];
		this.ysteptable = new float[this.angles.ANGLE360 + 1];
	}
	
	// The Player object needs the sin and cos trig tables for movement calculations.
	public void passtrigtables(Player p) {
		p.sintable = this.sintable;
		p.costable = this.costable;
	}
	
	// Used by GameWindow.fillvbuffer() to get information for ceiling/floor texturing.
	public int fetchraydata() {
		return this.castangle;
	}
	
	// Given a position on the map and an angle, cast out 320 rays for rendering and enemy sight.
	public void castRays(int angle, int x, int y) {
        int castangle; // Cast 320 rays across player.angle - 30 to player.angle + 30.
        int castcol; // Cast column (for rendering)
        int distancetotheprojectionplane = ((int)((angles.PROJECTIONPLANEWIDTH / 2) / (tantable[angles.ANGLE30])));

    	castangle = (angle - angles.ANGLE30);
        if (castangle < 0) {
        	castangle = angles.ANGLE360 + castangle; }
        for (castcol = 0; castcol < angles.PROJECTIONPLANEWIDTH; castcol += 5) {
        	castRay(castangle, x, y, castcol, distancetotheprojectionplane); 
        	castangle += 5;
        	if (castangle >= angles.ANGLE360) {
                castangle -= angles.ANGLE360; 
            } 
        }
        renderer.repaint();
	}
	
	// Cast out a ray
	public void castRay(int castangle, int x, int y, int castcol, int distancetotheprojectionplane) {
		/*
			The basic process:
				1. Calculate the first intercept points where the ray hits the grid.
				2. Calculate the distances between intercepts.
				3. Run through each ray until we hit a wall. Record the distance and other information about
					the cell we hit.
				4. See which one is shorter, pass relevant info to GameWindow.drawWallSlice().
		*/
		
		// Lists of cells the ray passes through.
		List<Integer> xraycells = new ArrayList<Integer>();
		List<Integer> yraycells = new ArrayList<Integer>();
		// 277
		// Distance to the horizonal wall
		float hgriddist;
		// Distance to the vertical wall
		float vgriddist;
		
		int vgrid;
		int hgrid;
		
		// Distances to the next intercept (should be TILE_SIZE or -TILE_SIZE)
		int disttonexthgrid;
		int disttonextvgrid;
		
		// Points where the ray is
		float rayypoint;
		float rayxpoint;
		
		// Map cells where the ray is (ray_point / TILE_SIZE)
	    int rayXxcell = x / TILE_SIZE;
	    int rayXycell = y / TILE_SIZE;
	    int rayYxcell = x / TILE_SIZE;
	    int rayYycell = y / TILE_SIZE;
	    
	    // Position on the wall tile where the ray hits (for image rendering)
	    int hwallpos = 0;
	    int vwallpos = 0;
	    
	    // Wall type
	    String hwalltype = "1";
	    String vwalltype = "1";
		
		// Find x intercepts
				
		// Get first intercept
		// Ray facing down
		if ((castangle > angles.ANGLE0) && (castangle < angles.ANGLE180)) {
			// Find the grid points for first intercept
			hgrid = ((y / TILE_SIZE) * TILE_SIZE) + TILE_SIZE;
			disttonexthgrid = TILE_SIZE;
			// Distance between intercepts
			float deltax = (hgrid - y) / tantable[castangle];
			
			rayxpoint = x + deltax;
		}
		// Ray facing up
		else {
			hgrid = ((y / TILE_SIZE) * TILE_SIZE);
			disttonexthgrid = -TILE_SIZE;
			
			float deltax = (hgrid - y) / tantable[castangle];
			
			rayxpoint = x + deltax;	
			hgrid--;
		}
		
		// Now look for walls
		
		// Avoid division by 0
	    if ((castangle == angles.ANGLE0) || (castangle == angles.ANGLE180)) {
	        hgriddist = 9999999F;
	    }
	    // Move the ray until we reach a horizontal wall.	    
	    else {
	    	float disttonextxintercept = xsteptable[castangle];
	        while (true) {
	        	rayXxcell = (int)(rayxpoint / TILE_SIZE);
	        	rayXycell = (hgrid / TILE_SIZE);
	        	xraycells.add((rayXycell * mapx) + rayXxcell);
	        	// Ray
	        	if ((rayXxcell >= mapx) || (rayXycell >= mapy) || (rayXxcell < 0) || (rayXycell < 0)) {
	        		hgriddist = Float.MAX_VALUE;
	        		break;
	        	}
	        	// It's a door.
	        	else if ((map[(rayXycell * mapx) + rayXxcell].mapchar) == '|') {
	        		// If we're not using images, ignore doors and treat them as open space.
	        		if (!useimages) {
		        		rayxpoint += disttonextxintercept;
		        		hgrid += disttonexthgrid; 
		        		continue;
	        		}
	        		// Determine which door texture to use
	        		if ((castangle > angles.ANGLE0) && (castangle < angles.ANGLE180)) {
	        			hwalltype = "|2";
	        		}
	        		else {
	        			hwalltype = "|2";
	        		}
	        		hgriddist = (rayxpoint - x) * inversecostable[castangle];
	        		hwallpos = ((int) rayxpoint % TILE_SIZE);
	        		int doorframes = this.mapptr.doormap.get((rayXycell * mapx) + rayXxcell).frames;
	        		// Ray hit a spot where the door is open, continue on
	        		if (hwallpos < doorframes) {
		        		rayxpoint += disttonextxintercept;
		        		hgrid += disttonexthgrid; 
	        		}
	        		else {
	        			hwallpos -= doorframes;
	        			break;
	        		}
	        	}
	        	// Not a door.
	        	else if ((map[(rayXycell * mapx) + rayXxcell].mapchar) != '0') {
	        		hgriddist = (rayxpoint - x) * inversecostable[castangle];
	        		hwallpos = ((int) (rayxpoint % TILE_SIZE));
	        		hwalltype = map[(rayXycell * mapx) + rayXxcell].mapstr;
	        		break;
	            }
	        	// else, the ray is not blocked, extend to the next block
	        	else {
	        		rayxpoint += disttonextxintercept;
	        		hgrid += disttonexthgrid; 
	        	}
	        	
	        }
	    }
		
		// Find y intercepts
	    
		// Ray facing left
	    if ((castangle < angles.ANGLE90) || (castangle > angles.ANGLE270)) {
			vgrid = ((x / TILE_SIZE) * TILE_SIZE) + TILE_SIZE;
			disttonextvgrid = TILE_SIZE;
			
			float deltay = tantable[castangle] * (vgrid - x);
			rayypoint = deltay + y;			
	    }
		// Ray facing right
	    else {
			vgrid = ((x / TILE_SIZE) * TILE_SIZE);
			disttonextvgrid = -TILE_SIZE;
			
			float deltay = tantable[castangle] * (vgrid - x);
			rayypoint = deltay + y;
			vgrid--;
	    }
	    
	    // Look for vertical wall

	    if ((castangle == angles.ANGLE90) || (castangle == angles.ANGLE270)) {
	        vgriddist = 9999999F;
	    }
	    else {
	        float disttonextyintercept = ysteptable[castangle];
	        while (true) {
	        	rayYxcell = (vgrid / TILE_SIZE);
	        	rayYycell = (int)(rayypoint / TILE_SIZE);
	        	yraycells.add((rayYycell * mapx) + rayYxcell);
	        	if ((rayYxcell >= mapx) || (rayYycell >= mapy) || (rayYxcell < 0) || (rayYycell < 0)) {
	        		vgriddist = Float.MAX_VALUE;
	        		break;
	        	}
	        	
	        	// It's a door.
	        	else if ((map[(rayYycell * mapx) + rayYxcell].mapchar) == '|') {
	        		// If we're not using images, ignore the doors.
	        		if (!useimages) {
		        		rayypoint += disttonextyintercept;
		        		vgrid += disttonextvgrid;
		        		continue;
	        		}
	        		// Determine which door texture to use
	        		if ((castangle > angles.ANGLE90) && (castangle < angles.ANGLE270)) {
	        			vwalltype = "|2";
	        		}
	        		else {
	        			vwalltype = "|2";
	        		}
	        		vgriddist = (rayypoint - y) * inversesintable[castangle];
	        		vwallpos = ((int) rayypoint % TILE_SIZE);
	        		int doorframes = this.mapptr.doormap.get((rayYycell * mapx) + rayYxcell).frames;
	        		// Ray hit a spot where the door is open, continue on
	        		if (vwallpos < doorframes) {
		        		rayypoint += disttonextyintercept;
		        		vgrid += disttonextvgrid;
		        		continue;
	        		}
	        		else {
	        			vwallpos -= doorframes;
	        			break;
	        		}
	        	}
	        	
	        	else if ((map[(rayYycell * mapx) + rayYxcell].mapchar) != '0') {
	        		vgriddist = (rayypoint - y) * inversesintable[castangle];
	        		vwallpos = ((int) (rayypoint % TILE_SIZE));
	        		vwalltype = map[(rayYycell * mapx) + rayYxcell].mapstr;
	        		break;
	        	}
	        	
	        	else {
	        		rayypoint += disttonextyintercept;
	        		vgrid += disttonextvgrid;
	        	}
	        }
	    }
	    
	    float dist;
	    String walltype;
	    int whichwall;
	    int wallpos;
	    int wallx;
	    int wally;
	    List<Integer> raycells = new ArrayList<Integer>();
	    
	    this.castangle = castangle;
	    
    	// Which wall is closer?
    	if (hgriddist < vgriddist) {
    		dist = hgriddist;
    		whichwall = 0;
    		walltype = hwalltype; 
    		wallpos = hwallpos;
    		wallx = rayXxcell;
    		wally = rayXycell;
    		raycells = xraycells;
    	}
    	else {
    		dist = vgriddist;
    		whichwall = 1;
    		walltype = vwalltype; 
    		wallpos = vwallpos;
    		wallx = rayYxcell;
    		wally = rayYycell;
    		raycells = yraycells;
    	}
	    
    	renderer.drawwallslice(walltype, wallx, wally, whichwall, wallpos, dist, castcol, distancetotheprojectionplane, raycells);
    }
}
